[Android] 折りたたみ可能なリストを作ってみた
レイアウトを折りたたみ可能にするライブラリ「ExpandableLayout」を使って、折りたたみ可能なリストを作ってみました。
はじめに
折りたたみ可能なリストをExpandableLayoutとRecyclerViewを使って実装します。
今回実装したソースコードはこちら↓
開発環境
- OS: MacOS Catalina
- Android Studio: 4.1.2
- Language: Kotlin 1.4.21
手順
- ライブラリを追加する
- レイアウトファイルの編集・追加
- Adapterの作成と実装
- MainActivityの実装
アプリレベルのbuild.gradleにライブラリを追加
ExpandableLayout、RecyclerView、CardViewのライブラリを追加します。
app/build.gradle
dependencies { // 省略 implementation 'net.cachapa.expandablelayout:expandablelayout:2.9.2' implementation "androidx.recyclerview:recyclerview:1.1.0" implementation "androidx.cardview:cardview:1.0.0" }
レイアウトの編集(MainActivity)
activity_main.xmlにRecyclerViewを追加します。
src/main/res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="16dp" app:layoutManager="LinearLayoutManager" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
レイアウトの追加(RecyclerViewのアイテム)
RecylerViewに表示するアイテムのレイアウトを新規作成します。ExpandableLayoutを使って折りたたみ可能なレイアウトを定義します。開閉のアニメーションが良い感じになるようにapp:el_durationでアニメーションの伸縮時間を300msに、app:el_parallaxでパララックスを0.3で指定しました。
src/main/res/layout/recycler_item.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:card_view="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content"> <androidx.cardview.widget.CardView android:id="@+id/card_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginLeft="16dp" android:layout_marginTop="8dp" android:layout_marginRight="16dp" app:cardUseCompatPadding="true" card_view:cardCornerRadius="8dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <LinearLayout android:id="@+id/title_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:id="@+id/title" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:padding="16dp" /> <ImageView android:id="@+id/expand_arrow" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginEnd="12dp" app:srcCompat="@drawable/expand_arrow" /> </LinearLayout> <net.cachapa.expandablelayout.ExpandableLayout android:id="@+id/expandable_layout" android:layout_width="match_parent" android:layout_height="wrap_content" app:el_duration="300" app:el_parallax="0.3"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="8dp" app:srcCompat="@mipmap/ic_launcher" /> <TextView android:id="@+id/detail" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:gravity="center" android:padding="16dp" /> </LinearLayout> </net.cachapa.expandablelayout.ExpandableLayout> </LinearLayout> </androidx.cardview.widget.CardView> </LinearLayout>
タップした時の矢印アイコンも準備しておきます。矢印アイコンの画像ファイルは各自で準備してください。
src/main/res/drawable/expand_arrow.xml
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/ic_arrow_down_24px" android:state_selected="true" /> <item android:drawable="@drawable/ic_arrow_up_24px" android:state_selected="false" /> </selector>
Adapterの実装 その1(リストに表示するデータを表すMyItem)
RecyclerViewで指定するAdapterを実装していきます。まず、リストに表示するデータを表すMyItemクラスを実装します。項目は、id、タイトル、詳細情報、開閉状態の4つとします。
src/main/java/com/mos1210/android/example/expandablelayout/MyItem.kt
data class MyItem( val id: Int, val title: String, val detail: String, var isExpanded: Boolean = false )
Adapterの実装 その2(Viewを再利用するためのViewHolder)
MyAdapterクラスを新規作成して、その中にRecyclerView.ViewHolderを継承したMyViewHolderクラスを実装します。リストアイテムタップ時のアニメーションと拡大縮小アイコンを回転させるアニメーションもここで実装します。アニメーションはタップ時のみに限定したいのでbind関数の中ではexpand関数をfalseで呼び出します。これで、リストのアイテムが開いた状態でスクロールしてもアニメーションOFFで表示されます。
src/main/java/com/mos1210/android/example/expandablelayout/MyAdapter.kt
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private var currentItem: MyItem? = null private val title: TextView = itemView.findViewById(R.id.title) private val detail: TextView = itemView.findViewById(R.id.detail) private val expandedLayout: ExpandableLayout = itemView.findViewById(R.id.expandable_layout) private val titleLayout: LinearLayout = itemView.findViewById(R.id.title_layout) private val arrow: ImageView = itemView.findViewById(R.id.expand_arrow) init { itemView.setOnClickListener { currentItem?.let { val expanded = it.isExpanded it.isExpanded = expanded.not() titleLayout.isSelected = expanded.not() expandedLayout.toggle() val anim = RotateAnimation( 0f, 180f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f ).apply { duration = 300 fillAfter = true } arrow.startAnimation(anim) } } } fun bind(item: MyItem) { currentItem = item title.text = currentItem?.title detail.text = currentItem?.detail if (item.isExpanded) { expandedLayout.expand(false) } else { expandedLayout.collapse(false) } titleLayout.isSelected = item.isExpanded.not() } }
Adapterの実装
MyAdapterにListAdapterを継承し、onCreateViewHolder及びonBindViewHolderを実装します。
class MyAdapter : ListAdapter<MyItem, MyAdapter.MyViewHolder>(MyDiffCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { return MyViewHolder( LayoutInflater.from(parent.context).inflate(R.layout.recycler_item, parent, false) ) } override fun onBindViewHolder(holder: MyViewHolder, position: Int) { val item: MyItem = getItem(position) holder.bind(item) } class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { // 省略 前節で実装紹介したため } } object MyDiffCallback : DiffUtil.ItemCallback<MyItem>() { override fun areItemsTheSame(oldItem: MyItem, newItem: MyItem): Boolean { return oldItem == newItem } override fun areContentsTheSame(oldItem: MyItem, newItem: MyItem): Boolean { return oldItem.id == newItem.id } }
MainActivityでリスト表示の実装
MainAcitivityでRecyclerViewのデータ生成とAdapterを指定します。ListAdapterクラスのsubmitListメソッドで作成したデータを投入します。
src/main/java/com/mos1210/android/example/expandablelayout/MainActivity.kt
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val list: MutableList<MyItem> = mutableListOf() for (i in 0..50) { list.add(MyItem(i, "title $i", "detail $i")) } val recyclerView: RecyclerView = findViewById(R.id.recycler_view) val myAdapter = MyAdapter() recyclerView.adapter = myAdapter myAdapter.submitList(list) } }
実行結果
アプリを実行すると画面が表示されます。
リストアイテムをタップするとアニメーション付きでアイテムが開きます。
まとめ
ExpandableLayoutとRecyclerViewを組み合わせて折りたたみ可能なリスト表示ができました。もっと簡単で良さそうな方法があるよ!などあればTwitterやコメントで教えていただければ嬉しいです。